PythonでUnboundLocalErrorに遭遇したら
こんちには。
データアナリティクス事業本部 機械学習チームの中村(nokomoro3)です。
本記事ではPythonの変数スコープと、UnboundLocalError
について整理したいと思います。
はじめに
Pythonでは、関数外の変数であっても、関数内で以下のように参照することができます。
var1 = "関数外の変数" def print_var1(): print(f"{var1=}") return print_var1()
var1='関数外の変数'
上記のようにインデントの最も浅い部分に定義された変数は、グローバル変数となります。
以下のコードでそれをチェックしてみましょう。
var1 = "関数外の変数" print(f"{var1=}, {id(var1)=}") def print_var1(): # globalかどうかを確認 if "var1" in globals().keys(): print("var1 is global") else: print("var1 is not global") print(f"{var1=}, {id(var1)=}") return print_var1() print(f"{var1=}, {id(var1)=}")
var1='関数外の変数', id(var1)=135576734702416 var1 is global var1='関数外の変数', id(var1)=135576734702416
var1はグローバル変数であり、オブジェクトIDも同じものとなっていることが分かります。
グローバル変数を書き換えようとすると
ただし、例えば以下のように参照しつつ再代入しようとするとエラーとなります。
var1 = "グローバル変数" print(f"{var1=}, {id(var1)=}") def print_var1_with_concat(): var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734933104 --------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-41-3041b5379d6e> in <cell line: 11>() 9 return 10 ---> 11 print_var1_with_concat() 12 print(f"{var1=}, {id(var1)=}") <ipython-input-41-3041b5379d6e> in print_var1_with_concat() 4 def print_var1_with_concat(): 5 ----> 6 var1 = var1 + "_結合したい文字列" 7 print(f"{var1=}, {id(var1)=}") 8 UnboundLocalError: local variable 'var1' referenced before assignment
これは関数内でvar1
がローカル変数と見なされるようになるためです。
こういったUnboundLocalError
に遭遇する機会は意外と経験があることかと思います。
ローカル変数を定義しなおすと
こちらは関数内で変数を定義しなおすとエラーがなくなります。
var1 = "グローバル変数" print(f"{var1=}, {id(var1)=}") def print_var1_with_concat(): var1 = "関数内の変数" var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734702528 var1='関数内の変数_結合したい文字列', id(var1)=135576734703088 var1='グローバル変数', id(var1)=135576734702528
しかしこれは元のグローバル変数を変更するわけではありません。
グローバル変数は書き変わっておらず、オブジェクトIDも関数内と関数外で異なることが分かります。
つまり、グローバル変数と同じ変数名のものを、ローカル変数として関数内で定義すると、ローカル変数と見なされていることが分かります。
ここまでの話を整理すると
先ほどのエラーとなるコードではvar1 = var1 + "_結合したい文字列"
という一行により、ローカル変数としての定義と参照を同時に行っています。
また定義前に参照が実行されるため、UnboundLocalError
が発生しているということとなります。
つまり、グローバル変数と同じ名前の変数を関数内でローカル変数として使用すると、ローカル変数と見なされ、ローカル変数と見なされたものを、定義前に参照しようとすることでエラーとなっています。
よって以下のようなコードも、もちろんエラーとなります。
var1 = "グローバル変数" print(f"{var1=}, {id(var1)=}") def print_var1_with_concat(): print(f"{var1=}, {id(var1)=}") # ここでエラー var1 = "関数内の変数" var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734933584 --------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-42-7e829d20b7e6> in <cell line: 14>() 12 return 13 ---> 14 print_var1_with_concat() 15 print(f"{var1=}, {id(var1)=}") <ipython-input-42-7e829d20b7e6> in print_var1_with_concat() 4 def print_var1_with_concat(): 5 ----> 6 print(f"{var1=}, {id(var1)=}") # ここでエラー 7 8 var1 = "関数内の変数" UnboundLocalError: local variable 'var1' referenced before assignment
グローバル変数を書き換えるには
先ほどのコードではグローバル変数は書き換えができなかったですが、グローバル変数を書き換えたい場合は(良いかどうかは置いておいて)、以下のようにglobal 変数名
と書くことで実現できます。
var1 = "グローバル変数" print(f"{var1=}, {id(var1)=}") def print_var1_with_concat(): global var1 var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() print(f"{var1=}, {id(var1)=}")
var1='グローバル変数', id(var1)=135576734703200 var1='グローバル変数_結合したい文字列', id(var1)=135576734703312 var1='グローバル変数_結合したい文字列', id(var1)=135576734703312
ちなみに、最初とオブジェクトIDが変わるのは再代入により新しい変数となっているだけで、str型がimmutableなためであるため、今回の変数スコープの話とは無関係となります。
またmutableなデータ型の場合、global 変数名
などと記述しなくとも参照さえできれば書き換えが可能となります。
(mutable、immutableについては機会があれば別途記事にしようと思います)
関数内の関数の場合もエラーとなりうる
これらの挙動はグローバル変数だけではなく、関数内の関数で、その親関数の変数を参照する際にも発生します。
def wrapper(): var1 = "wrapper内の変数" print(f"{var1=}, {id(var1)=}") def print_var1_with_concat(): var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() return wrapper()
var1='wrapper内の変数', id(var1)=135576734703088 --------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-36-86d9896b7e57> in <cell line: 16>() 14 return 15 ---> 16 wrapper() 1 frames <ipython-input-36-86d9896b7e57> in print_var1_with_concat() 6 def print_var1_with_concat(): 7 ----> 8 var1 = var1 + "_結合したい文字列" 9 print(f"{var1=}, {id(var1)=}") 10 UnboundLocalError: local variable 'var1' referenced before assignment
wrapper内で定義された変数が、その中の関数でも変数として定義されているため、未定義前に参照しに行くこととなり、先ほどと同様にUnboundLocalError
となることがわかります。
親関数の変数を書き換えたい場合はnonlocal
このような場合に、親関数の変数を書き換えたい時は、global 変数
の代わりにnonlocal 変数
を記載する必要があります。
def wrapper(): var1 = "wrapper内の変数" print(f"{var1=}, {id(var1)=}") def print_var1_with_concat(): nonlocal var1 var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() print(f"{var1=}, {id(var1)=}") return wrapper()
var1='wrapper内の変数', id(var1)=135576734702528 var1='wrapper内の変数_結合したい文字列', id(var1)=135576734652976 var1='wrapper内の変数_結合したい文字列', id(var1)=135576734652976
関数内の関数内の関数でnonlocalを使うと?
では関数内の関数内の関数でnonlocalを使うとどうなるか?気になるところですが、これはひとつ外側の変数を取ってくるという挙動のようです。
def wrapper_of_wrapper(): var1 = "wrapperのwrapper内の変数" def wrapper(): var1 = "wrapper内の変数" def print_var1_with_concat(): nonlocal var1 var1 = var1 + "_結合したい文字列" print(f"{var1=}, {id(var1)=}") return print_var1_with_concat() return wrapper() wrapper_of_wrapper()
var1='wrapper内の変数_結合したい文字列', id(var1)=135576734651440
まとめ
いかがでしたでしょうか。本ブログがPythonの変数スコープやUnboundLocalError
に遭遇した方の参考になれば幸いです。